﻿using Sustainalytics.ClientDW.DataAccessLayer;
using Sustainalytics.ClientDW.Entities;
using Sustainalytics.ClientDW.Entities.Universes;
using Sustainalytics.Companies;
using Sustainalytics.Controversies.BusinessLogic;
using Sustainalytics.DataLayer.Shared;
using Sustainalytics.DynamicsCRMGateway;
using Sustainalytics.DynamicsCRMGateway.Entities;
using Sustainalytics.Entities;
using Sustainalytics.Entities.ESGRatings;
using Sustainalytics.MongoDB.Extensions;
using Sustainalytics.Portfolios.BL;
using Sustainalytics.Synchronization.Interface;
using Sustainalytics.Utils.Configuration;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Account = Sustainalytics.DynamicsCRMGateway.Entities.Account;
using AccountFeatureEnum = Sustainalytics.Auth.Claims.AccountFeature;

namespace Sustainalytics.Universes.Synchronization
{
    public class UniversesFromCrmSyncStep : ISyncStep
    {
        private SyncContext _syncContext;

        private IUniverseInfo _universesInfo;

        public SyncStepOrder ExecutionOrder => SyncStepOrder.UniversesFromCrm;

        public SyncContext Start(SyncContext syncContext)
        {
            _syncContext = syncContext;
            var maxParalelism = SettingsHelper.Read("MaxDegreeOfParallelism", Environment.ProcessorCount);
            var _bulkSize = SettingsHelper.Read("CrmBulkSize", 1000);
            var crmConnString = syncContext.GAConnectionStrings["CRM"];
            var clientDwConnString = syncContext.GAConnectionStrings["ClientDW"];
            var companyMatchingLogic = new CompanyMatchingLogic(MongoFactory.GetCollectionFromDatabase<CorporateData>(clientDwConnString));

            _universesInfo = new UniverseInfo(maxParalelism, _bulkSize, crmConnString, clientDwConnString);

            var systemportfolioRepository = new RepositoryCollection<Portfolio>(clientDwConnString);
            var securitiesRepository = new RepositoryCollection<PortfolioDetails>(clientDwConnString);
            var portfolioMatching = new PortfolioMatching(
                companyMatchingLogic,
                new CorporateDataUniversesLoader(MongoFactory.GetCollectionFromDatabase<CorporateData>(clientDwConnString)));

            CollectDataFromCrmAndDB(syncContext);

            var t0 = Task.Run(() =>
            {
                var systemPortfolioManager = new SystemPortfolioManager(_universesInfo);
                var systemPortfolios = systemPortfolioManager.CreateSystemPortfolios();
                syncContext.Log("Began updating system portfolios");
                var systemPortfolioManagement = new SystemPortfolioManagement(
                    companyMatchingLogic,
                    portfolioMatching,
                    systemportfolioRepository,
                    securitiesRepository);

                Stopwatch watch = Stopwatch.StartNew();
                systemPortfolioManagement.RemoveAllAutomaticSystemPortfolios();
                var time2 = watch.ElapsedMilliseconds;
                syncContext.Log(
                    String.Format("Removed all automatic system portfolios took {0} s", time2 / 100.0));

                watch.Restart();
                var automaticSystemPortfoliosInfo = systemPortfolioManagement.Add(systemPortfolios);
                var time3 = watch.ElapsedMilliseconds;
                syncContext.Log(
                    String.Format(
                        "Added {0} automatic system portfolios",
                        automaticSystemPortfoliosInfo.Count));
                syncContext.Log(
                    String.Format("Began updating manual system portfolios took: {0} s", time3 / 100.0));

                watch.Restart();
                systemPortfolioManagement.UpdateManualSystemPortfolios(automaticSystemPortfoliosInfo);
                var time5 = watch.ElapsedMilliseconds;
                syncContext.Log(String.Format("Update manual system portfolios took: {0} s", time5 / 100.0));

                watch.Restart();
                systemPortfolioManagement.RemoveAllEmptyManualSystemPortfolios();
                var time6 = watch.ElapsedMilliseconds;
                syncContext.Log(
                    String.Format("Ended updating manual system portfolios, it took:{0} s", time6 / 100.0));
            });

            var accountWithProductUniverseSet = GetAccountWithProductUniverseSet();
            var universes = GetUniverses(accountWithProductUniverseSet);

            var accountProductCoverage = GetAccountProductCoverages(accountWithProductUniverseSet);
            SaveAccountProductCoveragess(syncContext, accountProductCoverage);

            var t1 = Task.Run(() =>
            {
                var universeCompanies = GetUniverseCompanies(
                    accountWithProductUniverseSet,
                    universes,
                    _universesInfo.ReferenceUniverseSet);
                var universeCompaniesExtended = ComputeExtendedUniversesForCompanies(universeCompanies);
                SyncToRepositoryCollection<UniverseCompany, Guid>(universeCompaniesExtended);
            });

            var t2 = Task.Run(() => SyncToRepositoryCollection<Universe, int>(universes));

            var t3 = Task.Run(() =>
            {
                var featureUniverses = GetFeatureUniverses(accountWithProductUniverseSet, universes);
                SyncToRepositoryCollection<FeatureUniverse, Guid>(featureUniverses);
            });

            Task.WaitAll(t0, t1, t2, t3);

            _universesInfo.Clean();
            _syncContext = null;

            syncContext.Status = SyncOperationStatus.Succeeded;
            return syncContext;
        }

        private void CollectDataFromCrmAndDB(SyncContext syncContext)
        {
            {
                _universesInfo.LoadData();

                syncContext.Log("CRM/Account = " + _universesInfo.AccountSet.Count() + " records");
                syncContext.Log("CRM/AccountUniverse = " + _universesInfo.AccountUniverseSet.Count() + " records");
                syncContext.Log("CRM/CompaniesGroup = " + _universesInfo.CompaniesGroupSet.Count() + " records");
                syncContext.Log(
                    "CRM/CompaniesGroupConstituent = " + _universesInfo.CompaniesGroupConstituentSet.Count()
                    + " records");
                syncContext.Log("CRM/Company = " + _universesInfo.CompanySet.Count() + " records");
                syncContext.Log("CRM/ProductUniverse = " + _universesInfo.ProductUniverseSet.Count() + " records");
                syncContext.Log(
                    "CRM/UniverseFeatureProductUniverseBridge = "
                    + _universesInfo.UniverseFeatureProductUniverseBridgeSet.Count() + " records");
                syncContext.Log("CRM/UniverseFeature = " + _universesInfo.UniverseFeatureSet.Count() + " records");
                syncContext.Log(
                    "CRM/CompaniesGroupAccountUniverseBridge = "
                    + _universesInfo.CompaniesGroupAccountUniverseBridgeSet.Count() + " records");
                syncContext.Log(
                    "ClientDW/ReferenceUniverses = " + _universesInfo.ReferenceUniverseSet.Count() + " records");
            }
        }

        private IEnumerable<UniverseCompany> ComputeExtendedUniversesForCompanies(
            IEnumerable<UniverseCompany> universeCompanies)
        {
            var corporateDataList =
                new RepositoryCollection<CorporateData>(_syncContext.GAConnectionStrings["ClientDW"]).ReadAll(
                    new string[] { "ResearchParentId", "CoverageIds" });

            return new UniverseResearchCoverage().ProcessCoverage(universeCompanies, corporateDataList);
        }

        private AccountWithProductUniverse[] GetAccountWithProductUniverseSet()
        {
            var result = (from account in _universesInfo.AccountSet
                          join accountUniverse in _universesInfo.AccountUniverseSet on account.Id equals
                              accountUniverse.AccountRef.Id
                          join productUniverse in _universesInfo.ProductUniverseSet on
                              accountUniverse.ProductUniverseRef.Id equals productUniverse.Id
                          select
                              new AccountWithProductUniverse()
                              {
                                  Account = account,
                                  AccountUniverse = accountUniverse,
                                  ProductUniverse = productUniverse
                              }).ToArray();
            _universesInfo.ProductUniverseSet = null;
            return result;
        }

        private IEnumerable<AccountProductCoverage> GetAccountProductCoverages(IEnumerable<AccountWithProductUniverse> accountWithProductUniverse)
        {
            var query = (from x in accountWithProductUniverse
                         join bridge in _universesInfo.CompaniesGroupAccountUniverseBridgeSet on x.AccountUniverse.Id equals bridge.AccountUniverseId
                         join companiesGroup in _universesInfo.CompaniesGroupSet on bridge.CompaniesGroupId equals companiesGroup.Id
                         select new
                         {
                             AccountId = x.Account.Id,
                             AccountName = x.Account.Name,
                             ResearchProduct = x.ProductUniverse.Name,
                             ReferenceUniverse = companiesGroup.Name,
                             companiesGroup.IsStandard
                         }).Distinct().ToList();

            var referenceUniverses = query
                .Where(x => x.IsStandard == true)
                .Select(x => new
                {
                    x.AccountId,
                    x.AccountName,
                    x.ResearchProduct,
                    x.ReferenceUniverse
                }).ToList();

            var customReferenceUniverses = query
                .Where(x => x.IsStandard == false)
                .GroupBy(x => new { x.AccountId, x.AccountName, x.ResearchProduct }, (key, group) => new
                {
                    key.AccountId,
                    key.AccountName,
                    key.ResearchProduct,
                    Count = group.Count()
                })
                .Where(x => x.Count > 0)
                .Select(x => new
                {
                    x.AccountId,
                    x.AccountName,
                    x.ResearchProduct,
                    ReferenceUniverse = "Custom"
                })
                .ToList();

            referenceUniverses.AddRange(customReferenceUniverses);

            return referenceUniverses
                .GroupBy(x => new { x.AccountId, x.AccountName, x.ResearchProduct })
                .Select(g =>
                        new AccountProductCoverage()
                        {
                            Id = Guid.NewGuid(),
                            AccountId = g.Key.AccountId,
                            AccountName = g.Key.AccountName,
                            ResearchProduct = g.Key.ResearchProduct,
                            ReferenceUniverses = g.Select(x => x.ReferenceUniverse).OrderBy(x => x).ToList()
                        })
                .ToArray();
        }

        private static void SaveAccountProductCoveragess(SyncContext syncContext, IEnumerable<AccountProductCoverage> documents)
        {
            var collection = MongoFactory.GetCollectionFromDatabase<AccountProductCoverage>(syncContext.GAConnectionStrings["ClientPlatform"]);

            collection.Database.DropCollection(typeof(AccountProductCoverage).Name);

            collection.InsertMany(documents);
        }

        private FeatureUniverse[] GetFeatureUniverses(
            AccountWithProductUniverse[] accountWithProductUniverseQuery,
            Universe[] universes)
        {
            var query = (from x in accountWithProductUniverseQuery
                         join bridge in _universesInfo.UniverseFeatureProductUniverseBridgeSet on
                             x.AccountUniverse.ProductUniverseRef.Id equals bridge.ProductUniverseId
                         join universeFeature in _universesInfo.UniverseFeatureSet on bridge.UniverseFeatureId equals
                             universeFeature.Id
                         select
                             new
                             {
                                 AccountId = x.Account.Id,
                                 UniverseName = x.ProductUniverse.Name,
                                 FeatureName = universeFeature.Name
                             }).Distinct();

            return (from x in query
                    join universe in universes on new { AccountId = x.AccountId, UniverseName = x.UniverseName } equals
                new { AccountId = universe.AccountId, UniverseName = universe.Name }
                    select new { AccountId = x.AccountId, FeatureName = x.FeatureName, UniverseId = universe.Id })
                .GroupBy(x => new { x.AccountId, x.FeatureName })
                .Where(x => Enum.TryParse(x.Key.FeatureName, out AccountFeatureEnum result))
                .Select(
                    grp =>
                    new FeatureUniverse()
                    {
                        AccountId = grp.Key.AccountId,
                        Feature = (AccountFeatureEnum)Enum.Parse(typeof(AccountFeatureEnum), grp.Key.FeatureName),
                        UniverseIds = grp.Select(x => x.UniverseId).OrderBy(x => x).ToList()
                    })
                .ToArray();
        }

        private IEnumerable<UniverseCompany> GetUniverseCompanies(
            AccountWithProductUniverse[] accountWithProductUniverse,
            Universe[] universes,
            ReferenceUniverse[] referenceUniverses)
        {
            var query1 = (from x in accountWithProductUniverse
                          join bridge in _universesInfo.CompaniesGroupAccountUniverseBridgeSet on
                              x.AccountUniverse.AccountUniverseId equals bridge.AccountUniverseId
                          join companiesGroup in _universesInfo.CompaniesGroupSet on bridge.CompaniesGroupId equals
                              companiesGroup.Id
                          where companiesGroup.IsStandard != null && companiesGroup.IsStandard.Value == true
                          select
                              new
                              {
                                  AccountId = x.Account.Id,
                                  UniverseName = x.ProductUniverse.Name,
                                  CompaniesGroupName = companiesGroup.Name
                              }).Distinct();

            var standardUniverseCompanies = (from x in query1
                                             join universe in universes on
                                                 new { AccountId = x.AccountId, UniverseName = x.UniverseName } equals
                                                 new { AccountId = universe.AccountId, UniverseName = universe.Name }
                                             join referenceUniverse in referenceUniverses on x.CompaniesGroupName equals
                                                 referenceUniverse.Name
                                             from company in referenceUniverse.Companies
                                             select
                                                 new UniverseCompany()
                                                 {
                                                     CompanyId = company.CompanyId,
                                                     AccountId = x.AccountId,
                                                     CIQID = company.CapitalIqId.ToString(),
                                                     UniverseId = universe.Id
                                                 }).DistinctBy(
                                                         x =>
                                                         new
                                                         {
                                                             x.CompanyId,
                                                             x.AccountId,
                                                             x.UniverseId
                                                         });

            var query2 = (from x in accountWithProductUniverse
                          join bridge in _universesInfo.CompaniesGroupAccountUniverseBridgeSet on
                              x.AccountUniverse.AccountUniverseId equals bridge.AccountUniverseId
                          join companiesGroup in _universesInfo.CompaniesGroupSet on bridge.CompaniesGroupId equals
                              companiesGroup.Id
                          join companiesGroupConstituent in _universesInfo.CompaniesGroupConstituentSet on
                              companiesGroup.Id equals companiesGroupConstituent.CompaniesGroupRef.Id
                          join company in _universesInfo.CompanySet on companiesGroupConstituent.CompanyRef.Id equals
                              company.CompanyId
                          where companiesGroup.IsStandard == null || companiesGroup.IsStandard.Value == false
                          select
                              new
                              {
                                  CompanyId = company.CompanyId.Value,
                                  AccountId = x.Account.Id,
                                  CIQID = company.CapitalIqId,
                                  UniverseName = x.ProductUniverse.Name
                              }).Distinct();

            var customUniverseCompanies = (from x in query2
                                           join universe in universes on
                                               new { AccountId = x.AccountId, UniverseName = x.UniverseName } equals
                                               new { AccountId = universe.AccountId, UniverseName = universe.Name }
                                           select
                                               new UniverseCompany()
                                               {
                                                   CompanyId = x.CompanyId,
                                                   AccountId = x.AccountId,
                                                   CIQID = x.CIQID,
                                                   UniverseId = universe.Id
                                               }).ToArray();

            return standardUniverseCompanies.Union(customUniverseCompanies).ToList();
        }

        private Universe[] GetUniverses(AccountWithProductUniverse[] AccountWithProductUniverseSet)
        {
            var nextUniverseId =
                new UniversesRepository(_syncContext.GAConnectionStrings["ClientUniverses"]).GetNextUniverseId();
            var universes =
                AccountWithProductUniverseSet.Select(
                    x =>
                    new
                    {
                        AccountId = x.Account.Id,
                        AccountName = x.Account.Name,
                        UniverseName = x.ProductUniverse.Name
                    }).Distinct()
                    //                .ToArray() // we need to sort in memory because the CRM can't order by related entities attributes
                    .OrderBy(x => x.AccountName)
                    .ThenBy(x => x.UniverseName)
                    .Select(
                        (x, index) =>
                        new Universe() { Id = nextUniverseId + index, AccountId = x.AccountId, Name = x.UniverseName })
                    .ToArray();
            return universes;
        }

        private void LogRepositoryCollectionOperation<T>(string message, Action action)
        {
            var typeName = typeof(T).Name;

            try
            {
                var stopwatch = Stopwatch.StartNew();
                action();
                stopwatch.Stop();

                _syncContext.Log(
                    string.Format(
                        "Finished {0} {1} in repository collection. Duration: {2}",
                        message,
                        typeName,
                        stopwatch.Elapsed));
            }
            catch (Exception ex)
            {
                _syncContext.Log(string.Format("Failed {0} {1} in repository collection!", message, typeName));
                _syncContext.Log(ex.ToString());
                throw;
            }
        }

        private void SyncToRepositoryCollection<T, TKey>(IEnumerable<T> data) where T : IEntity<TKey>
        {
            var clientUniversesConnectionString = _syncContext.GAConnectionStrings["ClientUniverses"];
            var universeCompanyRepositoryCollection = new RepositoryCollection<T, TKey>(clientUniversesConnectionString);
            LogRepositoryCollectionOperation<T>(
                "delete all",
                () => { universeCompanyRepositoryCollection.DropAndCreate(); });
            LogRepositoryCollectionOperation<T>(
                "create batch",
                () => { universeCompanyRepositoryCollection.CreateBatchParallel(data, 5000); });
        }

        private class AccountWithProductUniverse
        {
            public Account Account { get; set; }

            public AccountUniverse AccountUniverse { get; set; }

            public ProductUniverse ProductUniverse { get; set; }
        }
    }
}
